package cn.kinoko.service.minio.impl;

import cn.kinoko.common.constant.Flag;
import cn.kinoko.common.utils.ExceptionUtil;
import cn.kinoko.config.minio.MinioProperties;
import cn.kinoko.model.minio.MinioObjReference;
import cn.kinoko.service.minio.MinioObjReferenceService;
import cn.kinoko.service.minio.MinioService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * minio服务
 *
 * @author kinoko
 */
@AllArgsConstructor
@Service
@Slf4j
public class MinioServiceImpl implements MinioService {

    private MinioClient minioClient;
    private MinioProperties minioProperties;
    private MinioObjReferenceService minioObjReferenceService;

    private final static String SEPARATOR = "/";

    /**
     * 构建文件路径
     *
     * @param dirPath  文件夹地址
     * @param filename yyyy/mm/dd/file.jpg
     * @return 文件全路径
     */
    private String builderFileDatePath(String dirPath, String filename, boolean datePrefix, boolean isThumbnail) {
        StringBuilder stringBuilder = new StringBuilder(50);
        if (isThumbnail) {
            stringBuilder.append(MinioProperties.THUMB_PREFIX);
        }
        if (!StringUtils.isEmpty(dirPath)) {
            stringBuilder.append(dirPath).append(SEPARATOR);
        }
        if (datePrefix) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
            String todayStr = sdf.format(new Date());
            stringBuilder.append(todayStr).append(SEPARATOR);
        }
        stringBuilder.append(filename);
        return stringBuilder.toString();
    }

    /**
     * 上传图片文件
     *
     * @param prefix      文件前缀
     * @param filename    文件名
     * @param inputStream 文件流
     * @param datePrefix  是否添加日期前缀
     * @return 文件全路径
     */
    @Override
    public String uploadFile(String prefix, String filename, String contentType, InputStream inputStream, boolean datePrefix) {
        return uploadFile(prefix, filename, contentType, inputStream, datePrefix, true, true);
    }

    /**
     * 上传文件
     *
     * @param prefix         文件前缀
     * @param filename       文件名
     * @param contentType    文件类型
     * @param inputStream    文件流
     * @param datePrefix     是否添加日期前缀
     * @param timeStamp      是否添加时间戳前缀
     * @param buildThumbnail 是否生成缩略图
     */
    @Override
    public String uploadFile(String prefix, String filename, String contentType, InputStream inputStream, boolean datePrefix, boolean timeStamp, boolean buildThumbnail) {
        // 加入时间戳，防止同名文件覆盖，并将空格替换成下划线
        filename = filename.replaceAll("\\s+", "_");
        if (timeStamp) {
            filename = System.currentTimeMillis() + "_" + filename;
        }
        // 构建文件存储路径
        String filePath = builderFileDatePath(prefix, filename, datePrefix, false);
        // 保存未引用文件
        minioObjReferenceService.saveUnReferenced(filePath);
        // 开始上传
        try (BufferedInputStream buffer = new BufferedInputStream(inputStream)) {
            // 记录读取位置，后续上传缩略图需要reset
            buffer.mark(0);
            // 构建请求
            PutObjectArgs.Builder builder = PutObjectArgs.builder()
                    .object(filePath)
                    .bucket(minioProperties.getBucket())
                    .stream(buffer, buffer.available(), -1);
            if (contentType != null) {
                builder.contentType(contentType);
            }
            // 上传高清图
            minioClient.putObject(builder.build());
            // 上传缩略图
            if (buildThumbnail) {
                uploadThumbImg(prefix, filename, contentType, datePrefix, buffer);
            }
            return minioProperties.getReadPath() +
                    SEPARATOR +
                    minioProperties.getBucket() +
                    SEPARATOR +
                    filePath;
        } catch (Exception ex) {
            log.error("minio put file error.", ex);
            throw new ExceptionUtil.BizException("上传文件失败");
        }
    }

    /**
     * 批量上传
     *
     * @param prefix     文件前缀
     * @param files      文件列表
     * @param datePrefix 是否添加日期前缀
     */
    @Override
    public List<String> uploadBatch(String prefix, MultipartFile[] files, boolean datePrefix) throws IOException {
        List<String> pathList = new ArrayList<>();
        for (MultipartFile file : files) {
            String path = uploadFile(prefix, file.getOriginalFilename(), file.getContentType(), file.getInputStream(), datePrefix);
            pathList.add(path);
        }
        return pathList;
    }

    /**
     * 删除文件
     *
     * @param pathUrl 文件全路径
     */
    @Override
    public void deleteFile(String pathUrl) {
        deleteFiles(Collections.singletonList(pathUrl));
    }

    /**
     * 批量删除文件
     *
     * @param pathUrls 文件全路径
     */
    @Override
    public void deleteFiles(Collection<String> pathUrls) {
        // 处理图片key和缩略图key http://xxx.cn/oss/bucketName/filePath -> /filePath & /thumb/filePath
        List<String> keyList = new ArrayList<>(pathUrls.stream().map(minioProperties::path2Key).toList());
        keyList.addAll(keyList.stream().map(key -> MinioProperties.THUMB_PREFIX + key).toList());
        // 批量删除key
        deleteFilesByKeys(keyList);
    }

    /**
     * 批量删除文件
     *
     * @param keys 文件key
     */
    @Override
    public void deleteFilesByKeys(Collection<String> keys) {
        // 构建删除Objects
        RemoveObjectsArgs removeObjectArgs = RemoveObjectsArgs.builder()
                .bucket(minioProperties.getBucket())
                .objects(keys.stream().map(DeleteObject::new).toList())
                .build();
        try {
            // 批量删除
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectArgs);
            // 必须调用迭代器才能删除
            for (Result<DeleteError> result : results) {
                DeleteError error = result.get();
                log.error("minio remove file error. key:{}, error:{}", error.objectName(), error.message());
            }
        } catch (Exception e) {
            log.error("minio remove file error.", e);
        }
    }

    /**
     * 下载文件
     *
     * @param pathUrl 文件全路径
     * @return 文件流
     */
    @Override
    public byte[] downLoadFileByteArr(String pathUrl) {
        String key = minioProperties.path2Key(pathUrl);
        try (InputStream is = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(minioProperties.getBucket())
                        .object(key)
                        .build())) {
            return is.readAllBytes();
        } catch (Exception e) {
            log.error("minio down file error.", e);
            return null;
        }
    }

    /**
     * 下载文件
     *
     * @param pathUrl 文件路径
     * @return 文件输入流
     */
    @Override
    public InputStream downLoadFileIs(String pathUrl) {
        String key = minioProperties.path2Key(pathUrl);
        try {
            return minioClient.getObject(GetObjectArgs.builder()
                    .bucket(minioProperties.getBucket())
                    .object(key)
                    .build());
        } catch (Exception e) {
            log.error("minio down file error.", e);
            return null;
        }
    }

    /**
     * 保存文件引用
     *
     * @param pathUrl 文件路径
     */
    @Override
    public void saveFileReference(String pathUrl) {
        saveFileReference(Collections.singletonList(pathUrl));
    }

    /**
     * 保存文件引用
     *
     * @param pathUrls 文件路径
     */
    @Override
    public void saveFileReference(Collection<String> pathUrls) {
        // 将地址转为key
        List<String> keyList = pathUrls.stream().map(minioProperties::path2Key).toList();
        // 更新引用状态
        minioObjReferenceService.lambdaUpdate()
                .in(MinioObjReference::getObjKey, keyList)
                .set(MinioObjReference::getStatus, Flag.YES)
                .update();
    }

    /**
     * 删除文件引用
     *
     * @param pathUrl 文件引用
     */
    @Override
    public void deleteFileReference(String pathUrl) {
        deleteFileReference(Collections.singletonList(pathUrl));
    }

    /**
     * 删除文件引用
     *
     * @param pathUrls 文件路径
     */
    @Override
    public void deleteFileReference(Collection<String> pathUrls) {
        List<String> keyList = pathUrls.stream().map(minioProperties::path2Key).toList();
        minioObjReferenceService.dereference(keyList);
    }

    /**
     * 清理无引用文件
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void clearNoReferenceFile() {
        // 查询引用状态为false的文件
        List<MinioObjReference> noReferenceFileList = minioObjReferenceService.lambdaQuery()
                .eq(MinioObjReference::getStatus, Flag.NO)
                .list();
        // 取出key
        List<String> keyList = new ArrayList<>(noReferenceFileList.stream().map(MinioObjReference::getObjKey).toList());
        List<String> thumbKeyList = keyList.stream().map(key -> MinioProperties.THUMB_PREFIX + key).toList();
        // 追加缩略图
        keyList.addAll(thumbKeyList);
        // 删除文件
        deleteFilesByKeys(keyList);
        // 删除引用记录
        List<Long> idList = noReferenceFileList.stream().map(MinioObjReference::getId).toList();
        minioObjReferenceService.removeByIds(idList);
    }

    /**
     * 上传缩略图
     *
     * @param prefix      文件前缀
     * @param filename    文件名
     * @param contentType 文件类型
     * @param datePrefix  是否添加日期前缀
     * @param buffer      文件流
     */
    private void uploadThumbImg(String prefix, String filename, String contentType, boolean datePrefix, BufferedInputStream buffer) {
        try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
            // 重置读取位置
            buffer.reset();
            // 缩略图路径
            String thumbnailPath = builderFileDatePath(prefix, filename, datePrefix, true);
            // 压缩图片
            Thumbnails.of(buffer)
                    .scale(minioProperties.getScale())
                    .outputFormat("jpeg")
                    .toOutputStream(byteOutput);
            byte[] byteArray = byteOutput.toByteArray();
            ByteArrayInputStream byteInput = new ByteArrayInputStream(byteArray);
            // 上传缩略图
            PutObjectArgs putThumbObjectArgs = PutObjectArgs.builder()
                    .object(thumbnailPath)
                    .contentType(contentType)
                    .bucket(minioProperties.getBucket())
                    .stream(byteInput, byteInput.available(), -1)
                    .build();
            minioClient.putObject(putThumbObjectArgs);
        } catch (Exception e) {
            log.error("minio put thumbnail file error: ", e);
        }
    }
}